package com.jerome.election.http.client;

import com.jerome.election.constants.AppConstant;
import com.jerome.election.http.HttpConstant;
import com.jerome.election.http.HttpMessageParser;
import com.jerome.election.http.HttpMethodEnum;
import com.jerome.election.http.Request;
import com.jerome.election.http.Response;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URL;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * HTTP Client for sending HTTP requests.
 *
 * @author Jerome
 * @date 2023/8/6
 */
public class HttpClient {

    /**
     * Send a GET request.
     *
     * @param urlStr  The URL to send the request to.
     * @param params  Request parameters.
     * @param headers Request headers.
     * @return The HTTP response.
     * @throws IOException If an I/O error occurs.
     */
    public static Response get(String urlStr, Map<String, String> params, Map<String, String> headers) throws IOException {
        return request(HttpMethodEnum.GET, buildQuery(urlStr, params), headers, null);
    }

    /**
     * Send a PUT request.
     *
     * @param urlStr  The URL to send the request to.
     * @param params  Request parameters.
     * @param headers Request headers.
     * @param body    Request body.
     * @return The HTTP response.
     * @throws IOException If an I/O error occurs.
     */
    public static Response put(String urlStr, Map<String, String> params, Map<String, String> headers, String body) throws IOException {
        headers.put("Content-Length", String.valueOf(body == null ? 0 : body.length()));
        return request(HttpMethodEnum.PUT, buildQuery(urlStr, params), headers, body);
    }

    /**
     * Send a DELETE request.
     *
     * @param urlStr  The URL to send the request to.
     * @param params  Request parameters.
     * @param headers Request headers.
     * @return The HTTP response.
     * @throws IOException If an I/O error occurs.
     */
    public static Response delete(String urlStr, Map<String, String> params, Map<String, String> headers) throws IOException {
        return request(HttpMethodEnum.DELETE, buildQuery(urlStr, params), headers, null);
    }

    /**
     * Build the query for the URL.
     *
     * @param urlStr The URL to build the query for.
     * @param params Request parameters.
     * @return The URL with the query.
     */
    private static String buildQuery(String urlStr, Map<String, String> params) {
        StringBuilder sb = new StringBuilder(urlStr);
        if (params != null && params.size() > 0) {
            if (!urlStr.contains(HttpConstant.URL_QUERY_IDENTIFIER)) {
                sb.append(HttpConstant.URL_QUERY_IDENTIFIER);
            }
            sb.append(params.entrySet().stream()
                    .map(x -> x.getKey() + HttpConstant.URL_QUERY_SEPARATOR + x.getValue())
                    .collect(Collectors.joining(HttpConstant.URL_QUERY_PARAMETER_SEPARATOR)));
        }
        return sb.toString();
    }

    /**
     * Send an HTTP request.
     *
     * @param methodEnum The HTTP method (GET, PUT, DELETE, POST).
     * @param urlStr     The URL to send the request to.
     * @param headers    Request headers.
     * @param body       Request body.
     * @return The HTTP response.
     * @throws IOException If an I/O error occurs.
     */
    public static Response request(HttpMethodEnum methodEnum, String urlStr, Map<String, String> headers, String body) throws IOException {
        URL url = new URL(urlStr);
        Request request = new Request(methodEnum.getCode(),
                url.getHost(),
                (url.getQuery() != null && !"".equals(url.getQuery())) ? (url.getPath() + "?" + url.getQuery()) : url.getPath(),
                headers,
                body);
        // Use SSLSocketFactory for HTTPS requests and Socket for HTTP requests
        Socket socket;
        if (urlStr.startsWith(AppConstant.HTTP_PREFIX)) {
            socket = SocketFactory.getDefault().createSocket(url.getHost(), url.getPort() == -1 ? 80 : url.getPort());
        } else {
            socket = SSLSocketFactory.getDefault().createSocket(url.getHost(), 443);
        }
        PrintWriter bw = new PrintWriter(socket.getOutputStream());
        sendRequest(bw, request);

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        Response response = HttpMessageParser.parseResponse(br);
        return response;
    }

    /**
     * Send an HTTP request.
     *
     * @param out     The output stream.
     * @param request The HTTP request.
     * @throws IOException If an I/O error occurs.
     */
    private static void sendRequest(PrintWriter out, Request request) throws IOException {
        out.println(request.getMethod() + " " + request.getUri() + " " + HttpConstant.HTTP_VERSION);
        out.println(String.format("Host: %s", request.getHost()));
        if (request.getHeaders() != null && request.getHeaders().size() > 0) {
            for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
                out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
        out.println();
        if (request.getMessage() != null && !"".equals(request.getMessage().trim())) {
            out.println(request.getMessage());
        }
        out.flush();
    }
}
